# -*- coding: utf-8 -*-

# python uses zlib http://zlib.net/

"""
The documents were writen strictly and gracefully.
"""

import sys
from struct import pack, unpack
import zlib

def coordinate(dword):
    res = 20.00
    return res
    
def parseoperation(flag, item):
    """
  0x0001 - queue command (shift held down)
  0x0002 - ? (apply to all units in subgroup ?)
  0x0004 - ? (area effect ?)
  0x0008 - group command (all units in current selection)
  0x0010 - move group without formation (formation disabled)
  0x0020 - ?
  0x0040 - ctrl held down (subgroup command)
         sometimes automatically added
         (e.g. subgroup worker -> rightclick mine without ctrl)

 new since patch 1.13
  0x0100 - autocast on/off command


Examples:
  0x42 - rightclick item in inventory
  0x44 - use ability (revive hero, cast spell)
  0x48 - ctrl group command (group peasants -> goldmine)

  0x44 - detonate Wisp (area effect unit ability ?)
  0x4C - use WON (area effect item ?)
  0x00 - use ability (Stop, Hold, uproot, Mirrorimage etc.)
  0x40 - Train unit / Research / Buy (resources changed?)
         (Orc burrow - combatpositions)
  0x42 - use special ability (that changes unit stats ???)
         (heros: skill ability, HU townhall: call to arms)
  0x44 - Summon (summon units and train heros)

TODO: further research, verification
    """

    if flag == 0x42:
        action = "rc item:"

class Pack:
    """
    stream
    """
    def __init__(self, buf):
        """
        buf is a string
        """
        self._rdptr = 0
        self._buf = buf
        self._size = len(buf)

    def _getbuf(self, size):
        self._rdptr += size
        if self._rdptr > self._size:
            print "---------------FINISHED------------------"
        else:
            return self._buf[self._rdptr - size : self._rdptr]

    def getbyte(self):
        buf = self._getbuf(1)
        res = unpack('B', buf)[0]
        return res

    def getword(self):
        buf = self._getbuf(2)
        res = unpack('H', buf)[0]
        return res

    def getdword(self):
        buf = self._getbuf(4)
        res = unpack('L', buf)[0]
        return res

    def getstr(self):
        index = self._buf.find('\x00', self._rdptr)
        l = index - self._rdptr
        buf = self._getbuf(l)
        res = unpack('<'+ str(l) + 's', buf)[0]
        self.getbyte()
        return res

    def getbuf(self, size):
        buf = self._getbuf(size)
        return buf

    def curpos(self):
        return self._rdptr

class ReplayHeader:
    """
    [Header]
    ===============================================================================
    offset | size/type | Description
    -------+-----------+-----------------------------------------------------------
    0x0000 | 28 chars  | zero terminated string "Warcraft III recorded game\0x1A\0"
    0x001c |  1 dword  | fileoffset of first compressed data block (header size)
           |           |  0x40 for WarCraft III with patch <= v1.06
           |           |  0x44 for WarCraft III patch >= 1.07 and TFT replays
    0x0020 |  1 dword  | overall size of compressed file
    0x0024 |  1 dword  | replay header version:
           |           |  0x00 for WarCraft III with patch <= 1.06
           |           |  0x01 for WarCraft III patch >= 1.07 and TFT replays
    0x0028 |  1 dword  | overall size of decompressed data (excluding header)
    0x002c |  1 dword  | number of compressed data blocks in file
    0x0030 |  n bytes  | SubHeader (see section 2.1 and 2.2)

    [SubHeader] for header version 1
    ===============================================================================
    offset | size/type | Description
    -------+-----------+-----------------------------------------------------------
    0x0000 |  1 dword  | version identifier string reading:
           |           |  'WAR3' for WarCraft III Classic
           |           |  'W3XP' for WarCraft III Expansion Set 'The Frozen Throne'
           |           | (note that this string is saved in little endian format
           |           |  in the replay file)
    0x0004 |  1 dword  | version number (corresponds to patch 1.xx so far)
    0x0008 |  1  word  | build number (see section 2.3)
    0x000A |  1  word  | flags
           |           |   0x0000 for single player games
           |           |   0x8000 for multiplayer games (LAN or Battle.net)
    0x000C |  1 dword  | replay length in msec
    0x0010 |  1 dword  | CRC32 checksum for the header
           |           | (the checksum is calculated for the complete header
           |           |  including this field which is set to zero)
    """

    def __init__(self, p):
        # header
        self.hint = p.getbuf(0x1c)
        self.offset = p.getdword()

        self.totalsize = p.getdword()
        self.headerversion = p.getdword()
        self.decompressedsize = p.getdword()
        self.blocknumber = p.getdword()

        # subheader
        version = p.getdword()
        if version == 0x57335850:
            self.version = "The Frozen Throne"
        else:
            self.version = "WarCraft III"
        self.versionnumber = p.getdword()
        self.buildnumber = p.getword()
        self.flags = p.getword()
        self.time = p.getdword()
        self.crc32 = p.getdword()

class BlockHeader:
    """
    [Data block header]
    ===============================================================================
    offset | size/type | Description
    -------+-----------+-----------------------------------------------------------
    0x0000 |  1  word  | size n of compressed data block (excluding header)
    0x0002 |  1  word  | size of decompressed data block (currently 8k)
    0x0004 |  1 dword  | unknown (probably checksum)
    0x0008 |  n bytes  | compressed data (decompress using zlib)
    """

    def __init__(self, p):
        self.csize = p.getword()
        self.dsize = p.getword()
        self.checksum = p.getdword()

class ReplayData:
    """
    [Decompressed data]
    ===============================================================================
     # |   Size   | Name
    ---+----------+--------------------------
     1 |   4 byte | Unknown (0x00000110 - another record id?)
     2 | variable | PlayerRecord (see 4.1)
     3 | variable | GameName (null terminated string) (see 4.2)
     4 |   1 byte | Nullbyte
     5 | variable | Encoded String (null terminated) (see 4.3)
       |          |  - GameSettings (see 4.4)
       |          |  - Map&CreatorName (see 4.5)
     6 |   4 byte | PlayerCount (see 4.6)
     7 |   4 byte | GameType (see 4.7)
     8 |   4 byte | LanguageID (see 4.8)
     9 | variable | PlayerList (see 4.9)
    10 | variable | GameStartRecord (see 4.11)
    """

    def __init__(self, buf):
        buf.getbuf(4)
        recordid = buf.getbyte()
        self.players = {}
        player = Player()
        player.baseinfo(buf)
        self.players[player.id] = player
        self.time = 0
        self.selectmode = 0x01
        self.teamscore = {}

        # [GameName]
        self.gamename = buf.getstr()
        buf.getbyte()

        # [GameSettings]
        p = Pack(self.decode(buf.getstr()))
        self.gamesettings = GameSettings(p)

        # [Map&CreatorName]
        self.mapname = p.getstr()
        self.mapcreator = p.getstr()

        p.getstr()

        # [PlayerCount]
        self.playercount = buf.getdword()

        # [GameType]
        gametype = buf.getbuf(4)
        gametype = unpack("B", gametype[0])[0]
        if gametype == 0x09:
            self.gametype = "Custom Game"
        elif gametype == 0x1d:
            self.gametype = "Single Player Game"
        else:
            self.gametype = "Unknown Game"

        # [LanguageID]
        self.languageid = buf.getbuf(4)

        # [PlayerList]
        while buf.getbyte() == 0x16:
            player = Player()
            player.baseinfo(buf)
            self.players[player.id] = player
            buf.getbuf(4)

        # [GameStartRecord]
        # recordid == 0x19
        size = buf.getword()
        self.playernumber = buf.getbyte()
        n = 0

        # [SlotRecord]
        while n < self.playernumber:
            playerid = buf.getbyte()
            if playerid == 0:
                buf.getbuf(8)
            else:
                player = self.players[playerid]
                player.slotinfo(buf)
            n += 1

        # [RandomSeed]
        self.randomseed = buf.getdword()
        selectmode = buf.getbyte()
        self.startspot = buf.getbyte()

        # [ReplayData]
        self.parserecord(buf)

    # get current time (return a string)
    def gettime(self):
        time = "{0:>02d}:{1:>02d}".format(
                self.time / 60000, self.time / 1000 % 60)
        return time

        # [ReplayData]
    def parserecord(self, buf):
        recordid = buf.getbyte()
        score = 0
        while True:
            # (unknown startblock)
            if recordid in [0x17]:
                reason = buf.getdword()
                playerid = buf.getbyte()
                result = buf.getdword()
                unknown = buf.getdword()
                player = self.players[playerid]
                if player.team in self.teamscore:
                    self.teamscore[player.team] += score
                else:
                    self.teamscore[player.team] = score
                player.setresult(reason, result, score, unknown)
                score += 10
            elif recordid in [0x1a, 0x1b, 0x1c]:
                buf.getdword()

            # (timeslot block)
            elif recordid in [0x1e, 0x1f]:
                self.parseslot(buf)
            elif recordid in [0x20]:
                playerid = buf.getbyte()
                size = buf.getword()
                flag = buf.getbyte()
                chatmode = buf.getdword()
                buf.getbuf(size - 5)
            elif recordid in [0x22]:
                buf.getbuf(5)
            elif recordid in [0x23]:
                buf.getbuf(10)
            elif recordid in [0x2f]:
                buf.getbuf(8)
            elif recordid in [0x00]:
                return
            else:
                print "------ERROR: %x------" % recordid
                break
            recordid = buf.getbyte()

    def parseslot(self, buf):
        size = buf.getword()
        slotpos = buf.curpos()
        self.time += buf.getword()
        while (buf.curpos() - slotpos) < size:
            playerid = buf.getbyte()
            player = self.players[playerid]
            length = buf.getword()
            actionpos = buf.curpos()
            while (buf.curpos() - actionpos) < length:
                self.parseaction(buf, player)

    def parseaction(self, buf, player):
            # read commanddata block
            actionid = buf.getbyte()
            action = ""

            if actionid != 0x16:
                self.selectmode = 0x01
                pass

            # pause game
            if actionid == 0x01:
                pass

            # resume game
            elif actionid == 0x02:
                pass

            # set gamespeed
            elif actionid == 0x03:
                self.gamespeed = buf.getbyte()
                pass

            # increase gamespeed
            elif actionid == 0x04:
                pass

            # decrease gamespeed
            elif actionid == 0x05:
                pass

            # save game
            elif actionid == 0x06:
                name = buf.getstr()
                pass

            # save game finished
            elif actionid == 0x07:
                buf.getdword()
                pass

            # unit/building ability (no additional parameter)
            elif actionid == 0x10:
                player.countapm()
                flag = buf.getword()
                item = buf.getdword()
                buf.getdword()
                buf.getdword()
                parseoperation(flag, item)
                pass

            #unit/building ability
            elif actionid == 0x11:
                player.countapm()
                flag = buf.getword()
                item = buf.getdword()
                buf.getdword()
                buf.getdword()
                x = buf.getdword()
                y = buf.getdword()
                pass

            # unit/building ability
            elif actionid == 0x12:
                player.countapm()
                flag = buf.getword()
                item = buf.getdword()
                buf.getdword()
                buf.getdword()
                x = coordinate(buf.getdword())
                y = coordinate(buf.getdword())
                obj1 = buf.getdword()
                obj2 = buf.getdword()
                action = "Right click: ({0:.2f}, {1:.2f})".format(x, y)
                pass

            # give/drop item
            elif actionid == 0x13:
                player.countapm()
                flag = buf.getword()
                item = buf.getdword()
                buf.getdword()
                buf.getdword()
                x = buf.getdword()
                y = buf.getdword()
                tobj1 = buf.getdword()
                tobj2 = buf.getdword()
                obj1 = buf.getdword()
                obj2 = buf.getdword()
                pass

            # unit/building ability
            elif actionid == 0x14:
                player.countapm()
                flag = buf.getword()
                item1 = buf.getdword()
                buf.getdword()
                buf.getdword()
                x1 = buf.getdword()
                y1 = buf.getdword()
                item2 = buf.getdword()
                buf.getbuf(9)
                x2 = buf.getdword()
                y2 = buf.getdword()
                pass

            # change selection (unit, building, area)
            elif actionid == 0x16:
                selectmode = buf.getbyte()
                no = buf.getword()
                if selectmode == 0x02:
                    player.countapm()
                    self.selectmode = 0x02
                    action = "Remove {0:d} unit(s) from selection".format(no)
                else:
                    if self.selectmode == 0x01:
                        player.countapm()
                    self.selectmode = 0x01
                    action = "Add {0:d} unit(s) to selection".format(no)
                i = 0
                while i < no:
                    obj1 = buf.getdword()
                    obj2 = buf.getdword()
                    i += 1
                    pass

            # assign group hotkey
            elif actionid == 0x17:
                player.countapm()
                groupid = buf.getbyte()
                no = buf.getword()
                i = 0
                while i < no:
                    obj1 = buf.getdword()
                    obj2 = buf.getdword()
                    i += 1
                    pass
                pass

            # select group hotkey
            elif actionid == 0x18:
                player.countapm()
                groupid = buf.getbyte()
                buf.getbyte()

                # select subgroup
            elif actionid == 0x19:
                item = buf.getdword()
                obj1 = buf.getdword()
                obj2 = buf.getdword()

                # pre subselection
            elif actionid == 0x1a:
                pass

            # unknown
            elif actionid == 0x1b:
                unknown = buf.getbyte()
                obj1 = buf.getdword()
                obj2 = buf.getdword()

                # select group item
            elif actionid == 0x1c:
                player.countapm()
                unknown = buf.getbyte()
                obj1 = buf.getdword()
                obj2 = buf.getdword()

                # cancel hero revival
            elif actionid == 0x1d:
                player.countapm()
                unit1 = buf.getdword()
                unit2 = buf.getdword()

                # remove unit from building queue
            elif actionid == 0x1e:
                player.countapm()
                slotnr = buf.getbyte()
                itemid = buf.getdword()

                # unknown
            elif actionid == 0x21:
                buf.getdword()
                bug.getdword()

                # single player cheats
            elif actionid in [0x20, 0x22, 0x23, 0x24, 0x25, 0x26, 0x29, 0x2a,
                    0x2b, 0x2c, 0x2f, 0x30, 0x31, 0x32]:
                pass
            elif actionid in [0x27, 0x28, 0x2d]:
                buf.getbuf(5)
            elif actionid in [0x2e]:
                buf.getbuf(4)

                # change ally options
            elif actionid == 0x50:
                nr = buf.getbyte()
                flag = buf.getdword()

                # transfer resources
            elif actionid == 0x51:
                nr = buf.getbyte()
                gold = buf.getdword()
                lumber = buf.getdword()

                # map trigger char command
            elif actionid == 0x60:
                buf.getdword()
                buf.getdword()
                buf.getstr()

                # ESC pressed
            elif actionid == 0x61:
                player.countapm()

                # scenario trigger
            elif actionid == 0x62:
                buf.getdword()
                buf.getdword()
                buf.getdword()

                # enter choose here skill submenu
            elif actionid == 0x66:
                player.countapm()

                # enter choose building submenu
            elif actionid == 0x67:
                player.countapm()

                # minimap signal
            elif actionid == 0x68:
                x = buf.getdword()
                y = buf.getdword()
                buf.getdword()

                # continue game
            elif actionid == 0x69:
                buf.getdword()
                buf.getdword()
                buf.getdword()
                buf.getdword()

                # continue game
            elif actionid == 0x6a:
                buf.getdword()
                buf.getdword()
                buf.getdword()
                buf.getdword()

                # unknown
            elif actionid == 0x75:
                buf.getbyte()

            else:
                print "-------PARSE ACTION ERROR ID: %02X---------" % actionid
                return

            if action != "":
                self.printaction(player.name, action)

    def decode(self, s):
        size = len(s)
        pos = 0
        dpos = 0
        mask = 0
        output = "" 
        while pos < size:
            if pos % 8 == 0:
                mask = ord(s[pos])
            elif mask & (0x01 << (pos % 8)) == 0x00:
                output += chr(ord(s[pos]) - 1)
                dpos += 1
            else:
                output += s[pos]
                dpos += 1
            pos += 1
        return str(output)

    # print action in string
    def printaction(self, name, des):
        print "%-10s %-16s %s" % (self.gettime(), name, des)

class Player:
    def __init__(self):
        self.actioncount = 0
        self.score = 0

    def baseinfo(self, p):
        """
        [PlayerRecord]
        ===============================================================================
        offset | size/type | Description
        -------+-----------+-----------------------------------------------------------
        0x0000 |  1 byte   | RecordID:
               |           |  0x00 for game host
               |           |  0x16 for additional players (see 4.9)
        0x0001 |  1 byte   | PlayerID
        0x0002 |  n bytes  | PlayerName (null terminated string)
           n+2 |  1 byte   | size of additional data:
               |           |  0x01 = custom
               |           |  0x08 = ladder
        """

        self.id = p.getbyte()
        self.name = p.getstr()
        self.additional = p.getbyte()

        """
        o For custom games:
        -------+-----------+---------------------------------------------------------
        offset | size/type | Description
        -------+-----------+---------------------------------------------------------
        0x0000 | 1 byte    | null byte (1 byte)
        o For ladder games:
        -------+-----------+---------------------------------------------------------
        offset | size/type | Description
        -------+-----------+---------------------------------------------------------
        0x0000 | 4 bytes   | runtime of players Warcraft.exe in milliseconds
        0x0004 | 4 bytes   | player race flags:
               |           |   0x01=human
               |           |   0x02=orc
               |           |   0x04=nightelf
               |           |   0x08=undead
               |           |  (0x10=daemon)
               |           |   0x20=random
               |           |   0x40=race selectable/fixed (see notes in section 4.11)
        """
        if (self.additional == 0x01):
            p.getbyte()
        elif (self.additional == 0x08):
            p.getbuf(4)
            self.race = p.getbuf(4)

    def slotinfo(self, p):
        """
        offset | size/type | Description
        -------+-----------+-----------------------------------------------------------
        0x0000 |  1 byte   | player id (0x00 for computer players)
        0x0001 |  1 byte   | map download percent: 0x64 in custom, 0xff in ladder
        0x0002 |  1 byte   | slotstatus:
               |           |   0x00 empty slot
               |           |   0x01 closed slot
               |           |   0x02 used slot
        0x0003 |  1 byte   | computer player flag:
               |           |   0x00 for human player
               |           |   0x01 for computer player
        0x0004 |  1 byte   | team number:0 - 11
               |           | (team 12 == observer or referee)
        0x0005 |  1 byte   | color (0-11):
               |           |   value+1 matches player colors in world editor:
               |           |   (red, blue, cyan, purple, yellow, orange, green,
               |           |    pink, gray, light blue, dark green, brown)
               |           |   color 12 == observer or referee
        0x0006 |  1 byte   | player race flags (as selected on map screen):
               |           |   0x01=human
               |           |   0x02=orc
               |           |   0x04=nightelf
               |           |   0x08=undead
               |           |   0x20=random
               |           |   0x40=race selectable/fixed (see notes below)
        0x0007 |  1 byte   | computer AI strength: (only present in v1.03 or higher)
               |           |   0x00 for easy
               |           |   0x01 for normal
               |           |   0x02 for insane
               |           | for non-AI players this seems to be always 0x01
        0x0008 |  1 byte   | player handicap in percent (as displayed on startscreen)
               |           | valid values: 0x32, 0x3C, 0x46, 0x50, 0x5A, 0x64
               |           | (field only present in v1.07 or higher)
        """
        self.mapdownloadpercent = p.getbyte()
        self.slotstatus = p.getbyte()
        flag = p.getbyte()
        if flag == 0x00:
            self.type = "Player"
        else:
            self.type = "Computer"
        self.team = p.getbyte() + 1
        self.color = p.getbyte()
        race = p.getbyte()
        if race & 0x01:
            self.race = "Hum"
        elif race & 0x02:
            self.race = "Orc"
        elif race & 0x04:
            self.race = "NE"
        elif race & 0x08:
            self.race = "UD"
        elif race & 0x40:
            self.race = "RD"
        else:
            self.race = "NA"
        self.ai = p.getbyte()
        self.handicap = p.getbyte()

    def setresult(self, reason, result, score, unknown):
        """
           Reason   | Result | Description
         ===========+========+=======================================================
            0x01    |  0x01  | player left (disconnected or saver is observer)    [O]
          (remote)  |  0x07  | player left
                    |  0x08  | player lost (was completly erased)
                    |  0x09  | player won
                    |  0x0A  | draw (long lasting tournament game)
                    |  0x0B  | player left (was observer)                         [?]
         -----------+--------+-------------------------------------------------------
            0x0E    |  0x01  | player left                                        [?]
          (remote)  |  0x07  | player left                                        [?]
                    |  0x0B  | player left (was observer)                         [?]
         ===========+========+=======================================================
            0x0C    |  0x01  | saver disc. / observer left                        [O]
          (not last)|  0x07  | saver lost, no info about the player               [?]
                    |  0x08  | saver lost (erased), no info about the player
                    |  0x09  | saver won, no info about the player
                    |  0x0A  | draw (long lasting tournament game)
                    |  0x0B  | saver lost (obs or obs on defeat)                  [?]
         -----------+--------+-------------------------------------------------------
          last 0x0C |  0x01  | saver disconnected
         (rep.saver)|  0x07  | with INC => saver won
                    |        | w/o  INC => saver lost
                    |  0x08  | saver lost (completly erased)
                    |  0x09  | saver won
                    |  0x0B  | with INC => saver won most times, but not always   [?]
                    |        | w/o  INC => saver left (was obs or obs on defeat)  [?]
        """
        self.result = result
        self.reason = reason
        self.score = score
        self.unknown = unknown
 
    def countapm(self):
        self.actioncount += 1

class GameSettings:
    """
    offset | bitnr | Description
    -------+-------+---------------------------------------------------------------
    0x0000 |  0,1  | Game Speed: 0 = slow, 1 = normal, 2 = fast, 3 = unused
    -------+-------+---------------------------------------------------------------
    0x0001 |   0   | Visibility: 'hide terrain'
           |   1   | Visibility: 'map explored'
           |   2   | Visibility: 'always visible' (no fog of war)
           |   3   | Visibility: 'default'
           |  4,5  | Observer  : 0 = off or 'Referees' (see 0x0003 Bit6)
           |       |             1 = unused
           |       |             2 = 'Obs on Defeat'
           |       |             3 = on or 'Referees'
           |   6   | Teams Together (team members are placed at neighbored places)
    -------+-------+---------------------------------------------------------------
    0x0002 |  1,2  | Fixed teams: 0 = off, 1 = unused, 2 = unused, 3 = on
    -------+-------+---------------------------------------------------------------
    0x0003 |   0   | Full Shared Unit Control
           |   1   | Random Hero
           |   2   | Random Races
           |   6   | Observer: Referees (other observer bits are 0 or 3)
    -------+-------+---------------------------------------------------------------
    0x0004 |       | 0
    0x0005 |       | unknown (0 in ladder games, but not in custom)
    0x0006 |       | 0
    0x0007 |       | unknown (0 in ladder games, but not in custom)
    0x0008 |       | 0
    -------+-------+---------------------------------------------------------------
    0x0009 | 4Byte | Map Checksum  //TODO: find algorithm
     -  0C |       |
    -------+-------+---------------------------------------------------------------
    """

    def __init__(self, p):
        self.gamespeed = p.getbyte()
        self.visibility = p.getbyte()
        self.fixedteams = p.getbyte()
        self.observer = p.getbyte()
        p.getbyte()
        p.getbyte()
        p.getbyte()
        p.getbyte()
        p.getbyte()
        p.getbuf(4)

       
class Replay:
    def __init__(self, name):
        self._filename = name
        self._data = ""
        self._rdptr = 0

    def parser(self):
        self._file = open(self._filename, 'rb')
        self.parseheader()
        self.parsebody()

    def _read(self, size):
        """
        return string
        """
        self._file.seek(self._rdptr)
        self._rdptr += size
        return self._file.read(size)
        
    def parseheader(self):
        p = Pack(self._read(0x44))
        self._header = ReplayHeader(p)

    def parsebody(self):
        i = 0
        while i < self._header.blocknumber: 
            i += 1
            p = Pack(self._read(0x08))
            b = BlockHeader(p)
            do = zlib.decompressobj()
            d = do.decompress(self._read(b.csize))
            if len(d) != b.dsize:
                print "-------DECOMPRESSED ERROR--------"
                return
            self._data += d
            pass
        self._body = ReplayData(Pack(self._data))

    def has_won(self, player):
        win_score = max(self._body.teamscore.values())
        team_score = self._body.teamscore[player.team]
        return (win_score == team_score)

    def output(self):
        # output essential infomation
        header = self._header
        body = self._body
        print """\
Basic Infomation
----------------
File Size: %d Bytes
Game Version: %s 1.%d build %d
Replay Length: %d Min %d Sec %d
Game Name: %s
Map: %s
Creator: %s
Game Type: %s 
Start Spot: %d
""" % (
                header.totalsize,
                header.version, header.versionnumber, header.buildnumber,
                header.time / 60000, header.time % 60000 / 1000, header.time % 1000 / 10,
                body.gamename,
                body.mapname,
                body.mapcreator,
                body.gametype,
                body.startspot
                )

        # output player(s) infomation
        print """\
Player(s) Infomation
--------------------
Slot   Name             Controller   Race   Team   Handicap   Actions   APM   Result\
"""
        for p in sorted(body.players.values()):
            print "%-6d %-16s %-12s %-6s %-6d %-10d %-9d %-6.2f  %d" %(
                    p.id, p.name, p.type, p.race, p.team, p.handicap,
                    p.actioncount, p.actioncount * 60 * 1000 / header.time, self.has_won(p))

if __name__ == '__main__':
    filename = sys.argv[1]
    replay = Replay(filename)
    replay.parser()
    replay.output()
